@@ -19,12 +19,11 @@ module TwitterConcern |
||
| 19 | 19 |
Twitter.configure do |config| |
| 20 | 20 |
config.consumer_key = options[:consumer_key] |
| 21 | 21 |
config.consumer_secret = options[:consumer_secret] |
| 22 |
- config.oauth_token = options[:oauth_token] |
|
| 23 |
- config.oauth_token_secret = options[:oauth_token_secret] |
|
| 22 |
+ config.oauth_token = options[:oauth_token] || options[:access_key] |
|
| 23 |
+ config.oauth_token_secret = options[:oauth_token_secret] || options[:access_secret] |
|
| 24 | 24 |
end |
| 25 | 25 |
end |
| 26 | 26 |
|
| 27 | 27 |
module ClassMethods |
| 28 |
- |
|
| 29 | 28 |
end |
| 30 | 29 |
end |
@@ -1,11 +1,12 @@ |
||
| 1 | 1 |
module Agents |
| 2 | 2 |
class TwitterStreamAgent < Agent |
| 3 |
+ include TwitterConcern |
|
| 3 | 4 |
cannot_receive_events! |
| 4 | 5 |
|
| 5 | 6 |
description <<-MD |
| 6 | 7 |
The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide. |
| 7 | 8 |
|
| 8 |
- You must provide an oAuth `consumer_key`, `consumer_secret`, `access_key`, and `access_secret`, as well as an array of `filters`. Multiple words in a filter |
|
| 9 |
+ You must provide an oAuth `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`, as well as an array of `filters`. Multiple words in a filter |
|
| 9 | 10 |
must all show up in a tweet, but are independent of order. |
| 10 | 11 |
|
| 11 | 12 |
To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). |
@@ -51,14 +52,10 @@ module Agents |
||
| 51 | 52 |
default_schedule "11pm" |
| 52 | 53 |
|
| 53 | 54 |
def validate_options |
| 54 |
- unless options[:consumer_key].present? && |
|
| 55 |
- options[:consumer_secret].present? && |
|
| 56 |
- options[:access_key].present? && |
|
| 57 |
- options[:access_secret].present? && |
|
| 58 |
- options[:filters].present? && |
|
| 55 |
+ unless options[:filters].present? && |
|
| 59 | 56 |
options[:expected_update_period_in_days].present? && |
| 60 | 57 |
options[:generate].present? |
| 61 |
- errors.add(:base, "expected_update_period_in_days, generate, consumer_key, consumer_secret, access_key, access_secret, and filters are required fields") |
|
| 58 |
+ errors.add(:base, "expected_update_period_in_days, generate, and filters are required fields") |
|
| 62 | 59 |
end |
| 63 | 60 |
end |
| 64 | 61 |
|
@@ -70,8 +67,8 @@ module Agents |
||
| 70 | 67 |
{
|
| 71 | 68 |
:consumer_key => "---", |
| 72 | 69 |
:consumer_secret => "---", |
| 73 |
- :access_key => "---", |
|
| 74 |
- :access_secret => "---", |
|
| 70 |
+ :oauth_token => "---", |
|
| 71 |
+ :oauth_token_secret => "---", |
|
| 75 | 72 |
:filters => %w[keyword1 keyword2], |
| 76 | 73 |
:expected_update_period_in_days => "2", |
| 77 | 74 |
:generate => "events" |
@@ -80,24 +77,34 @@ module Agents |
||
| 80 | 77 |
|
| 81 | 78 |
def process_tweet(filter, status) |
| 82 | 79 |
if options[:generate] == "counts" |
| 83 |
- # Avoid memory pollution |
|
| 84 |
- me = Agent.find(id) |
|
| 85 |
- me.memory[:filter_counts] ||= {}
|
|
| 86 |
- me.memory[:filter_counts][filter.to_sym] ||= 0 |
|
| 87 |
- me.memory[:filter_counts][filter.to_sym] += 1 |
|
| 88 |
- me.save! |
|
| 80 |
+ # Avoid memory pollution by reloading the Agent. |
|
| 81 |
+ agent = Agent.find(id) |
|
| 82 |
+ agent.memory[:filter_counts] ||= {}
|
|
| 83 |
+ agent.memory[:filter_counts][filter.to_sym] ||= 0 |
|
| 84 |
+ agent.memory[:filter_counts][filter.to_sym] += 1 |
|
| 85 |
+ remove_unused_keys!(agent, :filter_counts) |
|
| 86 |
+ agent.save! |
|
| 89 | 87 |
else |
| 90 | 88 |
create_event :payload => status.merge(:filter => filter.to_s) |
| 91 | 89 |
end |
| 92 | 90 |
end |
| 93 | 91 |
|
| 94 | 92 |
def check |
| 95 |
- if memory[:filter_counts] && memory[:filter_counts].length > 0 |
|
| 93 |
+ if options[:generate] == "counts" && memory[:filter_counts] && memory[:filter_counts].length > 0 |
|
| 96 | 94 |
memory[:filter_counts].each do |filter, count| |
| 97 | 95 |
create_event :payload => { :filter => filter.to_s, :count => count, :time => Time.now.to_i }
|
| 98 | 96 |
end |
| 99 |
- memory[:filter_counts] = {}
|
|
| 100 |
- save! |
|
| 97 |
+ end |
|
| 98 |
+ memory[:filter_counts] = {}
|
|
| 99 |
+ end |
|
| 100 |
+ |
|
| 101 |
+ protected |
|
| 102 |
+ |
|
| 103 |
+ def remove_unused_keys!(agent, base) |
|
| 104 |
+ if agent.memory[base] |
|
| 105 |
+ (agent.memory[base].keys - agent.options[:filters].map(&:to_sym)).each do |removed_key| |
|
| 106 |
+ agent.memory[base].delete(removed_key) |
|
| 107 |
+ end |
|
| 101 | 108 |
end |
| 102 | 109 |
end |
| 103 | 110 |
end |
@@ -17,11 +17,11 @@ require 'pp' |
||
| 17 | 17 |
def stream!(filters, options = {}, &block)
|
| 18 | 18 |
stream = Twitter::JSONStream.connect( |
| 19 | 19 |
:path => "/1/statuses/#{(filters && filters.length > 0) ? 'filter' : 'sample'}.json#{"?track=#{filters.map {|f| CGI::escape(f) }.join(",")}" if filters && filters.length > 0}",
|
| 20 |
- :oauth => {
|
|
| 20 |
+ :oauth => {
|
|
| 21 | 21 |
:consumer_key => options[:consumer_key], |
| 22 | 22 |
:consumer_secret => options[:consumer_secret], |
| 23 |
- :access_key => options[:access_key], |
|
| 24 |
- :access_secret => options[:access_secret] |
|
| 23 |
+ :access_key => options[:oauth_token] || options[:access_key], |
|
| 24 |
+ :access_secret => options[:oauth_token_secret] || options[:access_secret] |
|
| 25 | 25 |
}, |
| 26 | 26 |
:ssl => true |
| 27 | 27 |
) |
@@ -60,7 +60,7 @@ def load_and_run(agents) |
||
| 60 | 60 |
end |
| 61 | 61 |
end |
| 62 | 62 |
|
| 63 |
- options = agents.first.options.slice(:consumer_key, :consumer_secret, :access_key, :access_secret) |
|
| 63 |
+ options = agents.first.options.slice(:consumer_key, :consumer_secret, :access_key, :oauth_token, :access_secret, :oauth_token_secret) |
|
| 64 | 64 |
|
| 65 | 65 |
recent_tweets = [] |
| 66 | 66 |
|
@@ -0,0 +1,96 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe Agents::TwitterStreamAgent do |
|
| 4 |
+ before do |
|
| 5 |
+ @opts = {
|
|
| 6 |
+ :consumer_key => "---", |
|
| 7 |
+ :consumer_secret => "---", |
|
| 8 |
+ :oauth_token => "---", |
|
| 9 |
+ :oauth_token_secret => "---", |
|
| 10 |
+ :filters => %w[keyword1 keyword2], |
|
| 11 |
+ :expected_update_period_in_days => "2", |
|
| 12 |
+ :generate => "events" |
|
| 13 |
+ } |
|
| 14 |
+ |
|
| 15 |
+ @agent = Agents::TwitterStreamAgent.new(:name => "HuginnBot", :options => @opts) |
|
| 16 |
+ @agent.user = users(:bob) |
|
| 17 |
+ @agent.save! |
|
| 18 |
+ end |
|
| 19 |
+ |
|
| 20 |
+ describe '#process_tweet' do |
|
| 21 |
+ context "when generate is set to 'counts'" do |
|
| 22 |
+ before do |
|
| 23 |
+ @agent.options[:generate] = 'counts' |
|
| 24 |
+ end |
|
| 25 |
+ |
|
| 26 |
+ it 'records counts' do |
|
| 27 |
+ @agent.process_tweet(:keyword1, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 28 |
+ @agent.process_tweet(:keyword2, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 29 |
+ @agent.process_tweet(:keyword1, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 30 |
+ |
|
| 31 |
+ @agent.reload |
|
| 32 |
+ @agent.memory[:filter_counts][:keyword1].should == 2 |
|
| 33 |
+ @agent.memory[:filter_counts][:keyword2].should == 1 |
|
| 34 |
+ end |
|
| 35 |
+ |
|
| 36 |
+ it 'removes unused keys' do |
|
| 37 |
+ @agent.memory[:filter_counts] = {:keyword1 => 2, :keyword2 => 3, :keyword3 => 4}
|
|
| 38 |
+ @agent.save! |
|
| 39 |
+ @agent.process_tweet(:keyword1, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 40 |
+ @agent.reload.memory[:filter_counts].should == {:keyword1 => 3, :keyword2 => 3}
|
|
| 41 |
+ end |
|
| 42 |
+ end |
|
| 43 |
+ |
|
| 44 |
+ context "when generate is set to 'events'" do |
|
| 45 |
+ it 'emits events immediately' do |
|
| 46 |
+ lambda {
|
|
| 47 |
+ @agent.process_tweet('keyword1', {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 48 |
+ }.should change { @agent.events.count }.by(1)
|
|
| 49 |
+ |
|
| 50 |
+ @agent.events.last.payload.should == {
|
|
| 51 |
+ :filter => 'keyword1', |
|
| 52 |
+ :text => "something", |
|
| 53 |
+ :user => {:name => "Mr. Someone"}
|
|
| 54 |
+ } |
|
| 55 |
+ end |
|
| 56 |
+ end |
|
| 57 |
+ end |
|
| 58 |
+ |
|
| 59 |
+ describe '#check' do |
|
| 60 |
+ context "when generate is set to 'counts'" do |
|
| 61 |
+ before do |
|
| 62 |
+ @agent.options[:generate] = 'counts' |
|
| 63 |
+ @agent.save! |
|
| 64 |
+ end |
|
| 65 |
+ |
|
| 66 |
+ it 'emits events' do |
|
| 67 |
+ @agent.process_tweet(:keyword1, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 68 |
+ @agent.process_tweet(:keyword2, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 69 |
+ @agent.process_tweet(:keyword1, {:text => "something", :user => {:name => "Mr. Someone"}})
|
|
| 70 |
+ |
|
| 71 |
+ lambda {
|
|
| 72 |
+ @agent.reload.check |
|
| 73 |
+ }.should change { @agent.events.count }.by(2)
|
|
| 74 |
+ |
|
| 75 |
+ @agent.events[-1].payload[:filter].should == 'keyword1' |
|
| 76 |
+ @agent.events[-1].payload[:count].should == 2 |
|
| 77 |
+ |
|
| 78 |
+ @agent.events[-2].payload[:filter].should == 'keyword2' |
|
| 79 |
+ @agent.events[-2].payload[:count].should == 1 |
|
| 80 |
+ |
|
| 81 |
+ @agent.memory[:filter_counts].should == {}
|
|
| 82 |
+ end |
|
| 83 |
+ end |
|
| 84 |
+ |
|
| 85 |
+ context "when generate is not set to 'counts'" do |
|
| 86 |
+ it 'does nothing' do |
|
| 87 |
+ @agent.memory[:filter_counts] = { :keyword1 => 2 }
|
|
| 88 |
+ @agent.save! |
|
| 89 |
+ lambda {
|
|
| 90 |
+ @agent.reload.check |
|
| 91 |
+ }.should_not change { Event.count }
|
|
| 92 |
+ @agent.memory[:filter_counts].should == {}
|
|
| 93 |
+ end |
|
| 94 |
+ end |
|
| 95 |
+ end |
|
| 96 |
+end |